package game.game.editors;

import com.crunch.core.Actor;
import com.crunch.core.Input;
import com.crunch.core.Renderable;
import com.crunch.graphics.gpu.GpuBlendState;
import com.crunch.graphics.gpu.GpuState;
import com.crunch.graphics.gpu.GpuTexture;
import com.crunch.math.Matrix33af;
import com.crunch.math.Vector2f;
import com.crunch.math.Vector4f;
import game.game.GameActor;
import game.game.RoomDefinition;
import game.game.RoomState;
import org.json.JSONObject;
import util.Camera;

import javax.swing.*;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.table.DefaultTableModel;
import java.awt.*;
import java.awt.event.*;
import java.io.File;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.*;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;

/**
 * Allows editing of game actors.
 */
public class ActorEditor extends Actor implements Renderable {
	public ActorEditor() {
		super(-50);
	}

	@Override
	public void onCreate() {
		getEngine().window.renderer.register(this);
		commandQueue = new ConcurrentLinkedQueue<Runnable>();

		findActorClasses("game.game");
		editor = new BackgroundEditorFrame();

		eventListener = new Input.EventListener() {
			@Override
			public void processEvent(AWTEvent e, int eventType) {
				if (eventType == Input.MOUSE_EVENT) {
					MouseEvent ev = (MouseEvent) e;
					if (ev.getButton() == MouseEvent.BUTTON2) {
						// camera drag
						if (ev.getID() == MouseEvent.MOUSE_PRESSED) {
							RoomState roomState = (RoomState) getGameState();
							int scale = roomState.getCoreState().getWindowScale();
							draggingCamera = true;
							dragX = ev.getX() / scale;
							dragY = ev.getY() / scale;
						} else if (ev.getID() == MouseEvent.MOUSE_RELEASED) {
							draggingCamera = false;
						}
					}
				} else if (eventType == Input.MOUSE_MOTION_EVENT) {
					MouseEvent ev = (MouseEvent) e;
					if (draggingCamera) {
						RoomState roomState = (RoomState) getGameState();
						int scale = roomState.getCoreState().getWindowScale();
						int newDragX = ev.getX() / scale;
						int newDragY = ev.getY() / scale;
						int dx = newDragX - dragX;
						int dy = dragY - newDragY; // flip y

						Camera camera = roomState.getCamera();
						int newX = (int) camera.getX() - dx;
						int newY = (int) camera.getY() - dy;

						// clamp to room size
						int xMin = newX - camera.getViewportWidth()/2;
						int yMin = newY - camera.getViewportHeight()/2;
						int xMax = xMin + camera.getViewportWidth();
						int yMax = yMin + camera.getViewportHeight();
						if (xMax > roomState.getRoomWidth()) {
							int diff = xMax - roomState.getRoomWidth();
							newX -= diff;
							xMin -= diff;
						}
						if (yMax > roomState.getRoomHeight()) {
							int diff = yMax - roomState.getRoomHeight();
							newY -= diff;
							yMin -= diff;
						}
						if (xMin < 0) {
							int diff = -xMin;
							newX += diff;
						}
						if (yMin < 0) {
							int diff = -yMin;
							newY += diff;
						}

						camera.set((float) newX, (float) newY, camera.getZoom());

						dragX = newDragX;
						dragY = newDragY;
					}
				}
			}
		};

		eventListener.eventMask = Input.MOUSE_EVENT | Input.MOUSE_MOTION_EVENT;
		getEngine().input.addEventListener(eventListener);
	}

	// initializes editor with existing game actor state
	public void init(RoomDefinition roomDefinition) {
		this.roomDefinition = roomDefinition;
		editor.init();
	}

	@Override
	public void onUpdate() {
		// run all commands
		Runnable cmd;
		while ((cmd = commandQueue.poll()) != null) {
			cmd.run();
		}
	}

	@Override
	public void onDestroy() {
		getEngine().window.renderer.unregister(this);
		editor.dispose();

		getEngine().input.removeEventListener(eventListener);
	}

	private RoomDefinition roomDefinition;
	private boolean draggingCamera = false;
	private int dragX, dragY;

	private Input.EventListener eventListener;

	private int actorSelection = -1;

	private String[] classes;

	// note: does NOT work with JARs
	private void findActorClasses(String packageName) {
		String packageDir = packageName.replace('.', '/');
		Enumeration<URL> resources;
		try {
			resources = getClass().getClassLoader().getResources(packageDir);
		} catch (IOException e) {
			throw new IllegalArgumentException("Invalid package " + packageName);
		}

		ArrayList<String> classList = new ArrayList<String>();
		while (resources.hasMoreElements()) {
			try {
				File resourceFile = new File(resources.nextElement().toURI());
				String resourceFilePrefix = resourceFile.getAbsolutePath();
				resourceFilePrefix = resourceFilePrefix.substring(0, resourceFilePrefix.length() - packageDir.length());
				classList.addAll(findClassesInFile(resourceFile, resourceFilePrefix));
			} catch (URISyntaxException e) {
				System.out.println("Invalid URI syntax");
			}
		}

		classes = new String[classList.size()];
		for (int i = 0; i < classes.length; ++i) {
			classes[i] = classList.get(i);
		}
	}

	private List<String> findClassesInFile(File file, String resourceFilePrefix) {
		final String CLASS_SUFFIX = ".class";

		List<String> classList = new ArrayList<String>();
		if (file.isDirectory()) {
			for (File nestedFile : file.listFiles()) {
				classList.addAll(findClassesInFile(nestedFile, resourceFilePrefix));
			}
		} else if (file.getName().endsWith(CLASS_SUFFIX)) {
			String fileStr = file.getAbsolutePath();
			if (!fileStr.startsWith(resourceFilePrefix)) {
				throw new IllegalStateException("Invalid file " + fileStr);
			}
			String classStr = fileStr.substring(resourceFilePrefix.length());
			classStr = classStr.replace(File.separatorChar, '.');
			classStr = classStr.substring(0, classStr.length() - CLASS_SUFFIX.length());
			if (isClassNameValid(classStr)) {
				classList.add(classStr);
			}
		}
		return classList;
	}

	private boolean isClassNameValid(String className) {
		try {
			// find the class by name
			Class<?> actorClass = Class.forName(className);
			// verify that it is derived from GameActor
			return GameActor.class.isAssignableFrom(actorClass);
		} catch (ClassNotFoundException e) {
			return false;
		}
	}

	@Override
	public void render(GpuState gpuState) {

		// render a sprite for each game actor with x,y
		RoomState roomState = (RoomState) getGameState();

		GpuTexture actorTexture = roomState.getCoreState().resourceManager.getTexture("actor");
		Vector4f color = new Vector4f(1.0f, 1.0f, 1.0f, 1.0f);
		Vector2f min = new Vector2f(0.0f, 0.0f);
		Vector2f max = new Vector2f((float) actorTexture.getWidth(), (float) actorTexture.getHeight());
		Vector2f orig = max.multiply(0.5f);

		GpuBlendState blendState = gpuState.createBlendState();
		blendState.setBlendFunction(GpuBlendState.BlendMode.ALPHA);
		blendState.bind();

		Matrix33af modelMatrix = new Matrix33af();

		for (int i = 0; i < roomDefinition.getActorCount(); ++i) {
			RoomDefinition.ActorDefinition def = roomDefinition.getActor(i);
			JSONObject state = def.getState();
			if (state.has("x") && state.has("y")) {
				Object x = state.get("x");
				Object y = state.get("y");
				if (x instanceof Number && y instanceof Number) {
					float xf = ((Number) x).floatValue() * RoomState.PHYSICS_SCALE;
					float yf = ((Number) y).floatValue() * RoomState.PHYSICS_SCALE;

					modelMatrix.setIdentity();
					modelMatrix.translateAssign(new Vector2f(xf, yf));

					Vector4f drawColor = color;
					if (actorSelection == i) {
						int ticks = roomState.getTicks();
						float ratio = (float) (ticks % 40) / 40.0f;
						float blink = Math.abs(ratio * 2.0f - 1.0f);
						drawColor = new Vector4f(1.0f, 1.0f, 1.0f, blink);
					}
					roomState.getSceneryRenderer().drawSprite(
							gpuState,
							roomState.getCamera().getViewProjectionMatrix().multiply(modelMatrix.getGraphicsMatrix()),
							actorTexture,
							drawColor,
							min, max, orig);
				}
			}
		}
	}

	@Override
	public int getDepth() {
		return Integer.MAX_VALUE - 10;
	}

	Queue<Runnable> commandQueue;

	BackgroundEditorFrame editor;
	private class BackgroundEditorFrame extends JFrame {
		private class Node {
			public RoomDefinition.ActorDefinition definition;
			public String text;
			public String toString() {
				return text;
			}
		}

		private class AttribNode {
			public String text;
			public String toString() {
				return text;
			}
		}

		private JButton addButton;
		private JButton removeButton;
		private JList<Node> list;
		private DefaultListModel<Node> model;
		private JComboBox<String> classNameComboBox;
		private JButton addAttributeButton;
		private JButton deleteAttributeButton;
		private JTable attributeTable;
		private AttributeTableModel attributeModel;

		private boolean performCallbacks;

		private class AttributeTableModel extends DefaultTableModel {
			public AttributeTableModel() {
			}

			@Override
			public int getColumnCount() {
				return 2;
			}

			@Override
			public String getColumnName(int columnIndex) {
				assert columnIndex >= 0 && columnIndex < 2;
				return columnIndex == 0 ? "Name" : "Value";
			}

			@Override
			public boolean isCellEditable(int rowIndex, int columnIndex) {
				return true;
			}

			@Override
			public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
				super.setValueAt(aValue, rowIndex, columnIndex);
				if (!performCallbacks || commandsPending > 0 || listSelection < 0) {
					return;
				}

				final ArrayList<String> names = new ArrayList<String>();
				for (int i = 0; i < getRowCount(); ++i) {
					names.add(i, (String) getValueAt(i, 0));
				}
				final String key = (String) getValueAt(rowIndex, 0);
				final int attribIndex = rowIndex;
				final String strValue = (String) aValue;
				final boolean isName = (columnIndex == 0);
				final RoomDefinition.ActorDefinition definition = model.get(listSelection).definition;

				enqueueCommand(new Runnable() {
					@Override
					public void run() {
						// validate name or value
						String strAssign1;
						JSONObject state = definition.getState();
						if (isName) {
							boolean nameChanged = didNameChange(names, state);
							if (!nameChanged) {
								// no change - don't need to do anything
								strAssign1 = strValue;
							} else {
								// name changed - make sure the new name is valid and not a duplicate
								boolean revert = !isNameValid(strValue) || isNameDuplicate(names);
								String oldName = getOldName(names, state);
								if (revert) {
									strAssign1 = oldName;
								} else {
									strAssign1 = strValue;
									// change the name
									state.put(strValue, state.remove(oldName));
								}
							}
						} else {
							// determine what type of value this is
							if (strValue.length() >= 2 && strValue.startsWith("\"") && strValue.endsWith("\"")) {
								// if it starts and ends with quotes, it is a string
								String value = strValue.substring(1, strValue.length()-1);
								state.put(key, value);
								strAssign1 = strValue;
							} else if (strValue.contains(".")) {
								// try to interpret it as a float
								try {
									double val = Double.parseDouble(strValue);
									state.put(key, val);
									strAssign1 = Double.toString(val);
								} catch (NumberFormatException e) {
									// revert
									strAssign1 = getStringForValue(state.get(key));
								}
							} else {
								// try to interpret it as an integer
								try {
									int val = Integer.parseInt(strValue);
									state.put(key, val);
									strAssign1 = Integer.toString(val);
								} catch (NumberFormatException e) {
									// revert
									strAssign1 = getStringForValue(state.get(key));
								}
							}
						}

						final String strAssign = strAssign1;

						finishedCommand(new Runnable() {
							@Override
							public void run() {
								boolean tempPerformCallbacks = performCallbacks;
								performCallbacks = false;
								try {
									attributeTable.setValueAt(strAssign, attribIndex, isName ? 0 : 1);
								} finally {
									performCallbacks = tempPerformCallbacks;
								}
							}
						});
					}
				});
			}

			private boolean didNameChange(List<String> names, JSONObject state) {
				HashSet<String> stateNames = new HashSet<String>();
				stateNames.addAll(state.keySet());
				for (String name : names) {
					if (!stateNames.remove(name)) {
						// a string in names was NOT found in state
						// if no name changed, there would be a 1-to-1 correspondence
						// therefore, a name must have changed
						return true;
					}
				}
				return false;
			}

			private boolean isNameValid(String name) {
				return !name.isEmpty();
			}

			private boolean isNameDuplicate(List<String> names) {
				// do simple n^2 comparison
				for (int a = 0; a < names.size(); ++a) {
					for (int b = a+1; b < names.size(); ++b) {
						if (names.get(a).equals(names.get(b))) {
							return true;
						}
					}
				}
				return false;
			}

			private String getOldName(List<String> names, JSONObject state) {
				HashSet<String> stateNames = new HashSet<String>();
				stateNames.addAll(state.keySet());
				stateNames.removeAll(names);
				// name MUST have changed
				assert !stateNames.isEmpty();
				return stateNames.iterator().next();
			}
		}

		private String getStringForValue(Object value) {
			String str = value.toString();
			if (value instanceof String) {
				str = "\"" + str + "\"";
			}
			return str;
		}

		public BackgroundEditorFrame() {
			setDefaultCloseOperation(DO_NOTHING_ON_CLOSE);

			setTitle("Actor editor");

			JPanel contentPanel = new JPanel();
			add(contentPanel);

			GroupLayout layout = new GroupLayout(contentPanel);
			contentPanel.setLayout(layout);
			layout.setAutoCreateGaps(true);
			layout.setAutoCreateContainerGaps(true);

			addButton = new JButton("Add");
			removeButton = new JButton("Remove");

			model = new DefaultListModel<Node>();
			list = new JList<Node>(model);
			JScrollPane listScroll = new JScrollPane(list);

			JLabel classNameLabel = new JLabel("Class:");
			classNameComboBox = new JComboBox<String>();
			classNameComboBox.setMinimumSize(new Dimension(64, classNameComboBox.getMinimumSize().height));
			for (String c : classes) {
				classNameComboBox.addItem(c);
			}
			classNameComboBox.setSelectedIndex(-1);

			addAttributeButton = new JButton("Add attribute");
			deleteAttributeButton = new JButton("Delete attribute");

			attributeModel = new AttributeTableModel();
			attributeTable = new JTable(attributeModel);
			attributeTable.getTableHeader().setReorderingAllowed(false);
			attributeTable.putClientProperty("terminateEditOnFocusLost", true);
			JScrollPane attribTableScroll = new JScrollPane(attributeTable);

			layout.setHorizontalGroup(layout.createParallelGroup()
					.addGroup(layout.createSequentialGroup()
							.addComponent(addButton)
							.addComponent(removeButton))
					.addComponent(listScroll)
					.addGroup(layout.createSequentialGroup()
							.addComponent(classNameLabel)
							.addComponent(classNameComboBox))
					.addGroup(layout.createSequentialGroup()
							.addComponent(addAttributeButton)
							.addComponent(deleteAttributeButton))
					.addComponent(attribTableScroll));
			layout.setVerticalGroup(layout.createSequentialGroup()
					.addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
							.addComponent(addButton)
							.addComponent(removeButton))
					.addComponent(listScroll)
					.addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
							.addComponent(classNameLabel)
							.addComponent(classNameComboBox))
					.addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
							.addComponent(addAttributeButton)
							.addComponent(deleteAttributeButton))
					.addComponent(attribTableScroll));

			// a hack to account for border sizes
			Insets insets = getInsets();
			setMinimumSize(new Dimension(320 + insets.left + insets.right, getMinimumSize().height));
			setSize(320 + insets.left + insets.right, 768 + insets.top + insets.bottom);

			addButton.addActionListener(new ActionListener() {
				@Override
				public void actionPerformed(ActionEvent e) {
					if (!performCallbacks || commandsPending > 0) {
						return;
					}
					enqueueCommand(new Runnable() {
						@Override
						public void run() {
							// create the actor definition
							final RoomDefinition.ActorDefinition def =
									new RoomDefinition.ActorDefinition();
							def.setClassName("");
							def.setState(new JSONObject());
							roomDefinition.addActor(roomDefinition.getActorCount(), def);

							finishedCommand(new Runnable() {
								@Override
								public void run() {
									// add to the list
									Node node = new Node();
									node.text = "null";
									node.definition = def;

									boolean tempPerformCallbacks = performCallbacks;
									performCallbacks = false;
									try {
										model.addElement(node);
									} finally {
										performCallbacks = tempPerformCallbacks;
									}
									list.setSelectedIndex(model.getSize() - 1);
								}
							});
						}
					});
				}
			});

			removeButton.addActionListener(new ActionListener() {
				@Override
				public void actionPerformed(ActionEvent e) {
					if (!performCallbacks || commandsPending > 0) {
						return;
					}
					if (listSelection < 0) {
						return;
					}
					// first enqueues the "deselect" command
					// current selection is stored, then removed
					final int oldSelection = listSelection;
					list.clearSelection();

					final Node node = model.get(listSelection);
					final RoomDefinition.ActorDefinition definition = node.definition;
					enqueueCommand(new Runnable() {
						@Override
						public void run() {
							// remove the actor definition
							for (int i = 0; i < roomDefinition.getActorCount(); ++i) {
								if (roomDefinition.getActor(i) == definition) {
									roomDefinition.removeActor(i);
									break;
								}
							}

							finishedCommand(new Runnable() {
								@Override
								public void run() {
									model.remove(oldSelection);
								}
							});
						}
					});
				}
			});

			list.addListSelectionListener(new ListSelectionListener() {
				@Override
				public void valueChanged(ListSelectionEvent e) {
					if (!performCallbacks) {
						return;
					}
					if (commandsPending > 0) {
						// revert to previous selection, as if nothing happened
						boolean tempPerformCallbacks = performCallbacks;
						performCallbacks = false;
						try {
							list.setSelectedIndex(listSelection);
						} finally {
							performCallbacks = tempPerformCallbacks;
						}
						return;
					}

					final String className = classNameComboBox.getSelectedIndex() < 0 ?
							"" : (String) classNameComboBox.getSelectedItem();
					final RoomDefinition.ActorDefinition oldDefinition = (listSelection < 0) ?
							null : model.get(listSelection).definition;
					final RoomDefinition.ActorDefinition newDefinition = (list.getSelectedIndex() < 0) ?
							null : model.get(list.getSelectedIndex()).definition;
					final int newSelection = list.getSelectedIndex();
					enqueueCommand(new Runnable() {
						@Override
						public void run() {
							final String classNameAssign;
							if (oldDefinition != null) {
								if (isClassNameValid(className)) {
									oldDefinition.setClassName(className);
									classNameAssign = className;
								} else {
									// reset class name
									classNameAssign = oldDefinition.getClassName();
								}
							} else {
								classNameAssign = null;
							}

							actorSelection = newSelection;

							// retrieve the new values
							final String className = (newDefinition == null) ? "" : newDefinition.getClassName();
							final JSONObject state = (newDefinition == null) ? null : newDefinition.getState();

							finishedCommand(new Runnable() {
								@Override
								public void run() {
									int oldSelection = listSelection;
									listSelection = list.getSelectedIndex();
									boolean tempPerformCallbacks = performCallbacks;
									performCallbacks = false;
									try {
										if (oldSelection >= 0) {
											assert classNameAssign != null;
											model.get(oldSelection).text = classNameAssign.isEmpty() ? "null" :
													classNameAssign;
											model.set(oldSelection, model.get(oldSelection)); // force repaint
										}
										if (className.isEmpty()) {
											classNameComboBox.setSelectedIndex(-1);
										} else {
											classNameComboBox.setSelectedItem(className);
										}

										// clear and rebuild table
										if (attributeTable.isEditing()) {
											attributeTable.getCellEditor().stopCellEditing();
										}
										while (attributeModel.getRowCount() > 0) {
											attributeModel.removeRow(0);
										}
										if (state != null) {
											for (Object key : state.keySet()) {
												String keyStr = (String) key;
												attributeModel.addRow(new Object[]{
														keyStr, getStringForValue(state.get(keyStr))});
											}
										}
									} finally {
										performCallbacks = tempPerformCallbacks;
									}
								}
							});
						}
					});
				}
			});

			classNameComboBox.addActionListener(new ActionListener() {
				@Override
				public void actionPerformed(ActionEvent e) {
					actorDefinitionChanged();
				}
			});

			addAttributeButton.addActionListener(new ActionListener() {
				@Override
				public void actionPerformed(ActionEvent e) {
					if (!performCallbacks || commandsPending > 0 || listSelection < 0) {
						return;
					}

					final RoomDefinition.ActorDefinition definition =
							model.get(listSelection).definition;

					enqueueCommand(new Runnable() {
						@Override
						public void run() {
							// create the actor definition
							JSONObject state = definition.getState();
							int suffix = 0;
							while (state.has("attrib" + suffix)) {
								++suffix;
							}
							final String key = "attrib" + suffix;
							state.put(key, 0);
							final String valueStr = getStringForValue(0);

							finishedCommand(new Runnable() {
								@Override
								public void run() {
									// add to the attribute table
									boolean tempPerformCallbacks = performCallbacks;
									performCallbacks = false;
									try {
										attributeModel.addRow(new Object[]{key, valueStr});
									} finally {
										performCallbacks = tempPerformCallbacks;
									}
								}
							});
						}
					});
				}
			});

			deleteAttributeButton.addActionListener(new ActionListener() {
				@Override
				public void actionPerformed(ActionEvent e) {
					if (!performCallbacks || commandsPending > 0 || listSelection < 0) {
						return;
					}

					final int[] rows = attributeTable.getSelectedRows();
					final String[] keys = new String[rows.length];
					for (int i = 0; i < rows.length; ++i) {
						keys[i] = (String) attributeModel.getValueAt(rows[i], 0);
					}
					final RoomDefinition.ActorDefinition definition =
							model.get(listSelection).definition;

					enqueueCommand(new Runnable() {
						@Override
						public void run() {
							// create the actor definition
							JSONObject state = definition.getState();
							for (String key : keys) {
								state.remove(key);
							}

							finishedCommand(new Runnable() {
								@Override
								public void run() {
									// add to the attribute table
									boolean tempPerformCallbacks = performCallbacks;
									performCallbacks = false;
									try {
										// delete rows in reverse
										for (int i = rows.length-1; i >= 0; --i) {
											attributeModel.removeRow(rows[i]);
										}
									} finally {
										performCallbacks = tempPerformCallbacks;
									}
								}
							});
						}
					});
				}
			});

			listSelection = -1;

			performCallbacks = true;

			setVisible(true);
		}

		private void init() {
			// make a copy to avoid concurrent modification errors
			final List<RoomDefinition.ActorDefinition> gameActors = new ArrayList<RoomDefinition.ActorDefinition>();
			final List<String> classNames = new ArrayList<String>();
			for (int i = 0; i < roomDefinition.getActorCount(); ++i) {
				gameActors.add(roomDefinition.getActor(i));
				classNames.add(roomDefinition.getActor(i).getClassName());
			}

			SwingUtilities.invokeLater(new Runnable() {
				@Override
				public void run() {
					performCallbacks = false;
					try {
						for (int i = 0; i < gameActors.size(); ++i) {
							Node node = new Node();
							node.definition = gameActors.get(i);
							node.text = classNames.get(i) == null ? "null" : classNames.get(i);
							model.addElement(node);
						}
					} finally {
						performCallbacks = true;
					}
				}
			});
		}

		private void actorDefinitionChanged() {
			if (!performCallbacks || commandsPending > 0 || listSelection < 0) {
				return;
			}

			final String className = (String) classNameComboBox.getSelectedItem();
			final RoomDefinition.ActorDefinition definition = model.get(listSelection).definition;
			enqueueCommand(new Runnable() {
				@Override
				public void run() {
					final String classNameAssign;
					if (isClassNameValid(className)) {
						definition.setClassName(className);
						classNameAssign = className;
					} else {
						// reset class name
						classNameAssign = definition.getClassName();
					}
					finishedCommand(new Runnable() {
						@Override
						public void run() {
							boolean tempPerformCallbacks = performCallbacks;
							performCallbacks = false;
							try {
								model.get(listSelection).text = classNameAssign.isEmpty() ? "null" : classNameAssign;
								model.set(listSelection, model.get(listSelection)); // force repaint
								// class name will be reset if it was invalid
								classNameComboBox.setSelectedItem(classNameAssign);
							} finally {
								performCallbacks = tempPerformCallbacks;
							}
						}
					});
				}
			});
		}

		private int listSelection;

		private int commandsPending = 0;
		private void enqueueCommand(Runnable command) {
			commandQueue.add(command);
			++commandsPending;
		}
		private void finishedCommand() {
			finishedCommand(null);
		}
		private void finishedCommand(final Runnable uiCommand) {
			SwingUtilities.invokeLater(new Runnable() {
				@Override
				public void run() {
					--commandsPending;
					if (uiCommand != null) {
						uiCommand.run();
					}
				}
			});
		}
	}
}
